using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Security.Cryptography;
using System.Net;
using System.IO;
using LibGreen.Jabber.Delegates;
using LibGreen.Jabber.ProtocolElements.SaslElements;
using LibGreen.Jabber.ProtocolElements;

namespace LibGreen.Jabber.Connection
{
    /// <summary>
    /// Contains SASL handshaking logic
    /// </summary>
    class SASLNegotiator
    {
        /// <summary>
        /// Describes the current state of a SASL negotiation session
        /// </summary>
        private enum NegotiationState
        {
            /// <summary>
            /// The negotiation has not started
            /// </summary>
            NotStarted,
            /// <summary>
            /// The &lt;auth&gt; element has been sent to the server
            /// </summary>
            SentAuth,
            /// <summary>
            /// A &lt;response&gt; has been sent to the server in response to a challenge
            /// </summary>
            SentResponse,
        }

        private readonly XmppConnection connection;
        private NegotiationState state = NegotiationState.NotStarted;
        private string activeMechanism;
        private int responsesSent;

        /// <summary>
        /// Raised when SASL negotiation has failed
        /// </summary>
        public event SaslNegotiationFailedHandler NegotiationFailed;
        /// <summary>
        /// Raised when SASL negotiation has completed successfully
        /// </summary>
        public event ConnectionFinishedHandler NegotiationSucceeded;

        /// <summary>
        /// Initializes a new SASLNegotiator for a connection
        /// </summary>
        /// <param name="connection">The connection on which to perform SASL handshaking</param>
        /// <exception cref="InvalidOperationException">
        /// Thrown when the connection is not in the <see cref="ConnectionState.Authenticating"/> state
        /// </exception>
        public SASLNegotiator(XmppConnection connection)
        {
            if (connection.State != ConnectionState.Authenticating)
            {
                throw new InvalidOperationException("The connection is in an invalid state for SASL negotiation");
            }

            this.connection = connection;
        }

        /// <summary>
        /// Begins the process of SASL negotiation
        /// </summary>
        /// <param name="saslStreamFeatures">The <see cref="FeaturesElement"/> received just prior to SASL negotiation</param>
        public void StartNegotiation(FeaturesElement saslStreamFeatures)
        {
            responsesSent = 0;

            // Preferred authentication order: DIGEST-MD5, PLAIN
            if (saslStreamFeatures.SupportedSASLMechanisms.Contains("DIGEST-MD5"))
            {
                activeMechanism = "DIGEST-MD5";
                SendDigestMd5Auth();
            }
            else if (saslStreamFeatures.SupportedSASLMechanisms.Contains("PLAIN"))
            {
                activeMechanism = "PLAIN";
                SendPlainAuth();
            }
        }

        /// <summary>
        /// Processes an XML stanza involved in SASL negotiation
        /// </summary>
        /// <param name="element">The received <see cref="Element"/></param>
        public void DataReceived(Element element)
        {
            switch (state)
            {
                case NegotiationState.SentAuth:
                    ProcessAuthReply(element);
                    break;

                case NegotiationState.SentResponse:
                    ProcessResponseReply(element);
                    break;
            }
        }

        /// <summary>
        /// Processes the server's reply to the &lt;auth&gt; command
        /// </summary>
        /// <param name="element">The received <see cref="Element"/></param>
        private void ProcessAuthReply(Element element)
        {
            string name = element.Name.ToLowerInvariant();

            if (name == "failure")
            {
                FailureElement failure = new FailureElement(element);
                OnNegotiationFailed(failure.Reason);
            }
            else if (name == "success")
            {
                SuccessElement success = new SuccessElement(element);
                OnNegotiationSucceeded();
            }
            else if (name == "challenge")
            {
                ChallengeElement challenge = new ChallengeElement(element);
                switch (activeMechanism)
                {
                    case "PLAIN":
                        SendPlainResponse();
                        break;
                    case "DIGEST-MD5":
                        SendDigestMd5Response(challenge);
                        break;
                }
                state = NegotiationState.SentResponse;
            }
        }

        /// <summary>
        /// Processes the server's reply to the original &lt;challenge&gt; command
        /// </summary>
        /// <param name="element">The received <see cref="Element"/></param>
        private void ProcessResponseReply(Element element)
        {
            string name = element.Name.ToLowerInvariant();
            if (name == "failure")
            {
                FailureElement failure = new FailureElement(element);
                OnNegotiationFailed(failure.Reason);
            }
            else if (name == "success")
            {
                SuccessElement success = new SuccessElement(element);
                OnNegotiationSucceeded();
            }
            else if (name == "challenge")
            {
                // Another challenge!  What fun!
                ChallengeElement challenge = new ChallengeElement(element);
                switch (activeMechanism)
                {
                    case "DIGEST-MD5":
                        SendDigestMd5Response(challenge);
                        break;
                }
            }
        }

        #region Auth element sending methods
        /// <summary>
        /// Sends an &lt;auth&gt; element for the PLAIN authentication mechanism
        /// </summary>
        /// <remarks>GTalk likes having the data that is (usually?) in the response contained in the auth element</remarks>
        private void SendPlainAuth()
        {
            StringBuilder builder = new StringBuilder();
            builder.Append('\0');
            builder.Append(connection.Parent.JID.Node);
            builder.Append('\0');
            builder.Append(connection.Parent.Password);

            string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(builder.ToString()));

            connection.SendXmppElement(AuthElement.Create("PLAIN", base64));
            state = NegotiationState.SentAuth;
        }

        /// <summary>
        /// Sends an &lt;auth&gt; element for the PLAIN authentication mechanism
        /// </summary>
        /// <remarks>GTalk likes having the data that is usually in the response contained in the auth element</remarks>
        private void SendDigestMd5Auth()
        {
            connection.SendXmppElement(AuthElement.Create(activeMechanism));
            state = NegotiationState.SentAuth;
        }
        #endregion

        #region Response element sending methods
        /// <summary>
        /// Creates and sends a &lt;response&gt; element for the PLAIN authentication mechanism
        /// </summary>
        private void SendPlainResponse()
        {
            StringBuilder builder = new StringBuilder();
            builder.Append('\0');
            builder.Append(connection.Parent.JID.Node);
            builder.Append('\0');
            builder.Append(connection.Parent.Password);

            connection.SendXmppElement(ResponseElement.Create(builder.ToString()));
            responsesSent++;
        }

        /// <summary>
        /// Creates and sends a &lt;response&gt; element for the DIGEST-MD5 authentication mechanism
        /// </summary>
        /// <param name="challenge">The received challenge</param>
        private void SendDigestMd5Response(ChallengeElement challenge)
        {
            // Break up the challenge data into something managable
            string challengestring = challenge.ChallengeData;
            bool inQuotes = false;
            int lastStartPos = 0;
            List<string> pairs = new List<string>();
            for (int i = 0; i < challengestring.Length; i++)
            {
                if (challengestring[i] == '"')
                {
                    inQuotes = !inQuotes;
                }
                else if (challengestring[i] == ',')
                {
                    if (!inQuotes)
                    {
                        pairs.Add(challengestring.Substring(lastStartPos, i - lastStartPos));
                        lastStartPos = i + 1;
                    }
                }
            }

            Dictionary<string, string> challengeData = new Dictionary<string, string>();
            foreach (string item in pairs)
            {
                string[] kvpairs = item.Split('=');
                challengeData[kvpairs[0]] = kvpairs[1].Trim('"');
            }

            string responseData = "";
            if (!challengeData.ContainsKey("rspauth"))
            {
                // Build the cnonce
                byte[] cnoncebuffer = new byte[32];
                RandomNumberGenerator.Create().GetBytes(cnoncebuffer);
                string cnonce = ToHexString(cnoncebuffer);

                // Build the NC and digest-uri values
                string nc = "00000001";
                string digestUri = "xmpp/" + connection.Server;

                // Build the response
                MD5 md5 = MD5.Create();
                byte[] h1 = md5.ComputeHash(Encoding.UTF8.GetBytes(String.Format("{0}:{1}:{2}",
                    connection.Parent.JID.Node, challengeData["realm"], connection.Parent.Password)));
                byte[] b1 = Encoding.ASCII.GetBytes(String.Format(":{0}:{1}", challengeData["nonce"], cnonce));
                byte[] h1b1 = new byte[h1.Length + b1.Length];
                Array.Copy(h1, 0, h1b1, 0, h1.Length);
                Array.Copy(b1, 0, h1b1, h1.Length, b1.Length);
                h1 = md5.ComputeHash(h1b1);
                byte[] h2 = md5.ComputeHash(Encoding.ASCII.GetBytes(String.Format("AUTHENTICATE:{0}", digestUri)));
                byte[] h3 = md5.ComputeHash(Encoding.UTF8.GetBytes(String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
                    ToHexString(h1), challengeData["nonce"], nc, cnonce, "auth", ToHexString(h2))));
                string response = ToHexString(h3);

                // Build the reply string
                responseData = String.Format(@"username=""{0}"", realm=""{1}"", nonce=""{2}"", cnonce=""{3}"", nc={4}, qop={5}, digest-uri={6}, charset={7}, response={8}",
                    connection.Parent.JID.Node, challengeData["realm"], challengeData["nonce"], cnonce, nc, "auth", digestUri, challengeData["charset"], response);
            }

            // Send the element
            connection.SendXmppElement(ResponseElement.Create(responseData));
            responsesSent++;
        }
        #endregion

        #region Utility methods
        /// <summary>
        /// Translates a byte buffer to a string of hexadecimal numbers
        /// </summary>
        /// <param name="buffer">A byte buffer to translate to a string</param>
        /// <returns>A string of hexadecimal numbers</returns>
        private static string ToHexString(byte[] buffer)
        {
            StringBuilder builder = new StringBuilder();
            foreach (byte b in buffer)
            {
                builder.Append(b.ToString("x2"));
            }
            return builder.ToString().ToLowerInvariant();
        }

        /// <summary>
        /// Raises the <see cref="NegotiationFailed"/> event
        /// </summary>
        /// <param name="reason">The reason for the failure</param>
        private void OnNegotiationFailed(SaslFailureReason reason)
        {
            if (NegotiationFailed != null)
            {
                NegotiationFailed(this, new SaslNegotiationFailedEventArgs(reason));
            }
        }

        /// <summary>
        /// Raises the <see cref="NegotiationSucceeded"/> event
        /// </summary>
        private void OnNegotiationSucceeded()
        {
            if (NegotiationSucceeded != null)
            {
                NegotiationSucceeded(this, new EventArgs());
            }
        }
        #endregion
    }
}
